package edu.cth.scrabbit.server.core;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;

import edu.cth.scrabbit.server.core.network.PlayerThread;
import edu.cth.scrabbit.server.core.network.ServerListener;
import edu.cth.scrabbit.shared.GameLetter;
import edu.cth.scrabbit.shared.GameLetterUtils;
import edu.cth.scrabbit.shared.GameState;
import edu.cth.scrabbit.shared.Player;
import edu.cth.scrabbit.shared.network.Statement;
import edu.cth.scrabbit.shared.network.Statement.Statement_Type;


/**
 * This class represents the entire functionality of the server. It responds to
 * requests from clients and keeps track of the game. Its logic is somewhat
 * spread out over its two subsystems - ServerListener which handles network
 * connectivity and WordCorrector which corrects words.
 * 
 * @author Anders Jonsson, Marcus Harryson
 * 
 */
public class ServerModel implements IServerModel {
	// Singleton pattern variable:
	private static IServerModel serverModel;

	// Game-relevant information:
	private ServerPlayer[] playerArray;
	private int currentPlayer = -1;
	private int numbersOfPasses = 0;
	private GameLetter[][] gameboard;
	private Stack<GameLetter> gameLetterStack;
	private boolean gameHasStarted = false;
	private boolean playerWasSuccessfullyAdded = false;

	// The two subsystems:
	private ServerListener listener;
	private IWordCorrector wc = new WordCorrector();

	// Private constructor (singleton pattern):
	private ServerModel() {
		playerArray = new ServerPlayer[4];
		createGameBoard();
		listener = new ServerListener(4567);
	}

	// Create the game board, filling it with "dummy" letters:
	private void createGameBoard() {
		gameLetterStack = GameLetterUtils.getShuffledGameLetters();
		gameboard = new GameLetter[17][17];
		for (int i = 0; i < 17; i++) {
			for (int j = 0; j < 17; j++) {
				gameboard[j][i] = new GameLetter();
			}
		}

	}

	// Attempt to add a player to the server
	@Override
	public void addPlayer(ServerPlayer player) {
		for (int i = 0; i < 4; i++) {
			if (playerArray[i] == null) {
				playerArray[i] = player;
				player.setPosition(i);
				playerWasSuccessfullyAdded = true;
				return;
			}
		}
		// There is not room for another player:
		playerWasSuccessfullyAdded = false;
	}

	// Singleton pattern method:
	public static IServerModel getInstance() {
		if (serverModel == null) {
			serverModel = new ServerModel();
		}
		return serverModel;
	}

	@Override
	public boolean wasPlayerAdded() {
		return playerWasSuccessfullyAdded;
	}

	// Have the network subsystem start listening for connections:
	@Override
	public void startListening() {
		listener.start();
	}

	// Called right after a connection has been established; a client is sending
	// a player's name information to this server:
	@Override
	public Statement playerConnected(ServerPlayer player, String nickname) {
		player.setName(nickname);
		return getGameStateStatement(player);
	}

	// Hand out a number of letters to a player:
	private void giveLetters(IServerPlayer player, int amount) {
		for (int i = 0; i < amount; i++) {
			if (!(gameLetterStack.isEmpty())) {
				player.giveLetter(gameLetterStack.pop());
			} else {
				player.giveLetter(new GameLetter());
			}
		}
	}

	// Return a GameState mirror of the game as it currently appears:
	private GameState getCurrentGameState(ServerPlayer player) {
		Player[] players = new Player[4];
		for (int i = 0; i < players.length; i++) {
			if (playerArray[i] != null) {
				players[i] = playerArray[i].toPlayer();
			}
		}

		GameLetter[] ownLetters = new GameLetter[7];
		for (int i = 0; i < player.getLetters().length; i++) {
			if (player.getLetters()[i] != null) {
				ownLetters[i] = player.getLetters()[i];
			} else {
				ownLetters[i] = new GameLetter();
			}
		}

		return new GameState(gameboard, players, ownLetters, currentPlayer,
				gameHasStarted, player.getPosition());
	}

	// Get the server player associated with a certain thread (used mainly by
	// the network subsystem):
	@Override
	public ServerPlayer getServerPlayer(PlayerThread thread) {
		for (ServerPlayer player : playerArray) {
			if (player.getThread() == thread) {
				return player;
			}
		}

		// Will not happen - the only way to create a ServerPlayer is with a
		// ServerThread
		return null;
	}

	// Advance the game to the next player in order:
	@Override
	public void nextPlayer() {
		int count = 0;
		for (IServerPlayer player : playerArray) {
			if (player != null) {
				count++;
			}
		}
		if (currentPlayer == count - 1) {
			currentPlayer = 0;
		} else {
			currentPlayer++;
		}
	}

	// A player has attempted to "pass".
	@Override
	public Statement pass(ServerPlayer player) {
		if (isPlayerTurn(player)) {
			if (numbersOfPasses >= getNumberOfPlayers() * 2) {
				// Too many passes! Game over!
				endGame();
			} else {
				numbersOfPasses++;
				nextPlayer();
			}
		}
		return getGameStateStatement(player);
	}

	private int getNumberOfPlayers() {
		int count = 0;
		for (IServerPlayer p : playerArray) {
			if (p != null) {
				count++;
			}
		}
		return count;
	}

	private boolean isPlayerTurn(IServerPlayer player) {
		return playerArray[currentPlayer] == player;
	}

	@Override
	public Statement startGame(ServerPlayer player) {
		// A player has attempted to start the game! Allow it if it was the
		// first player only (the host player)
		if (player == playerArray[0] && !gameHasStarted) {
			createGameBoard();
			currentPlayer = 0;
			numbersOfPasses = 0;
			gameHasStarted = true;

			for (IServerPlayer p : playerArray) {
				if (p != null) {
					// hand out 7 letters to each player
					p.setScore(0);
					giveLetters(p, 7);
				}
			}
		}

		return getGameStateStatement(player);
	}

	@Override
	public void endGame() {
		gameHasStarted = false;
	}

	// A player has attempted to perform the "swap" action:
	@Override
	public Statement swap(ServerPlayer player) {
		if (isPlayerTurn(player) && gameLetterStack.size() > 7) {
			// add player' letters to pot.
			for (GameLetter gl : player.getLetters()) {
				player.removeLetter(gl);
				gameLetterStack.push(gl);
			}
			// Shuffle the letters and give back seven new ones.
			Collections.shuffle(gameLetterStack);
			giveLetters(player, 7);
			numbersOfPasses = 0;
			nextPlayer();
		}
		return getGameStateStatement(player);
	}

	// Get a statement containing the current game state, used extensively
	// within this class
	@Override
	public Statement getGameStateStatement(ServerPlayer player) {
		return new Statement(Statement_Type.SERVER_GAME_UPDATE,
				getCurrentGameState(player));
	}
	// Adds the given letter to the gameboard at position x and y
	private void addLetterToGameboard(GameLetter gl, int x, int y) {
		gameboard[y][x] = gl;
		gl.setPosition(x, y);
	}

	// A player has attempted to perform the "play" action:
	@Override
	public Statement play(ServerPlayer player, List<GameLetter> list) {
		if (isPlayerTurn(player)) {
			// Correct the placement:
			int points = correctWord(list);
			if (points != 0) {
				// correctWord succeeded
				player.givePoints(points);
				for (GameLetter gl : list) {
					addLetterToGameboard(gl, gl.getXIndex(), gl.getYIndex());
					player.removeLetter(gl);
					giveLetters(player, 1);
				}

				// See if we should end the game now:
				int letterCount = 0;
				for (GameLetter gl : player.getLetters()) {
					if (!gl.isDummy()) {
						letterCount++;
					}
				}

				if (letterCount == 0) {
					// We had no letters to give to a player, and the player has
					// no
					// letters left to play. Game over!
					endGame();
				}

				// Reset the pass counter and advance the game:
				numbersOfPasses = 0;
				nextPlayer();
			}
		}
		return getGameStateStatement(player);
	}

	// Check a player's entered letters to see if this is an allowed move -
	// returns the total score of this move:
	private int correctWord(List<GameLetter> enteredLetters) {
		// Retrieve all relevant words:
		List<Word> words = findWords(enteredLetters);

		int pointsCount = 0;
		for (Word w : words) {
			List<GameLetter> letters = new ArrayList<GameLetter>();
			if (w.isHorizontal()) {
				int y = w.getHighestY();
				for (int i = w.getLowestX(); i <= w.getHighestX(); i++) {
					GameLetter gameboardLetter = gameboard[y][i];
					if (!gameboardLetter.isDummy()) {
						letters.add(gameboardLetter);
						pointsCount += GameLetterUtils
								.getPoints(gameboardLetter);
					} else {
						// We did not find the letter on the game board, it has
						// to be among the entered letters
						for (GameLetter gl : enteredLetters) {
							if (gl.hasPosition(i, y)) {
								letters.add(gl);
								pointsCount += GameLetterUtils.getPoints(gl);
							}
						}

					}
				}

			} else if (w.isVertical()) {
				int x = w.getHighestX();
				for (int i = w.getLowestY(); i <= w.getHighestY(); i++) {
					GameLetter gameboardLetter = gameboard[i][x];
					if (!gameboardLetter.isDummy()) {
						letters.add(gameboardLetter);
						pointsCount += GameLetterUtils
								.getPoints(gameboardLetter);
					} else {
						// We did not find the letter on the game board, it has
						// to be among the entered letters
						for (GameLetter gl : enteredLetters) {
							if (gl.hasPosition(x, i)) {
								letters.add(gl);
								pointsCount += GameLetterUtils.getPoints(gl);
							}
						}

					}
				}
			}
			if (!wc.correctWord(letters)) {
				// The move was not allowed - return a score of 0
				return 0;
			}
		}

		return pointsCount;
	}

	// Find all words on the game board "connected to" the entered letters:
	private List<Word> findWords(List<GameLetter> enteredLetters) {
		List<Word> words = new ArrayList<Word>();

		// Instantiate highest/lowest values and set them to their respective
		// "negative extremes"
		int highestY = Integer.MIN_VALUE;
		int lowestY = Integer.MAX_VALUE;
		int highestX = Integer.MIN_VALUE;
		int lowestX = Integer.MAX_VALUE;

		// Calculate these values:
		for (GameLetter gl : enteredLetters) {
			highestY = highestY > gl.getYIndex() ? highestY : gl.getYIndex();
			highestX = highestX > gl.getXIndex() ? highestX : gl.getXIndex();
			lowestY = lowestY < gl.getYIndex() ? lowestY : gl.getYIndex();
			lowestX = lowestX < gl.getXIndex() ? lowestX : gl.getXIndex();
		}

		if (highestX == lowestX && highestY != lowestY) {
			// the word is vertical

			// Start by finding all words that cross the letters added by the
			// player
			for (GameLetter gl : enteredLetters) {
				int x = gl.getXIndex();
				int y = gl.getYIndex();
				int tempLX = x;
				int tempHX = x;
				if (x < 16 && !gameboard[y][x + 1].isDummy()) {
					tempHX = x + countRight(gl);
				}
				if (x > 0 && !gameboard[y][x - 1].isDummy()) {
					tempLX = x - countLeft(gl);
				}

				if (tempHX != tempLX) {
					words.add(new Word(tempHX, tempLX, y, y));
				}
			}

			// Special case: there are letters after the word:
			int x = highestX;
			int y = highestY + 1;
			while (y <= 16 && !gameboard[y][x].isDummy()) {
				enteredLetters.add(gameboard[y][x]);
				gameboard[y][x].setPosition(x, y);
				highestY++;
				y++;
			}

			// Special case: there are letters before the word.
			y = lowestY - 1;
			while (y >= 0 && !gameboard[y][x].isDummy()) {
				enteredLetters.add(gameboard[y][x]);
				gameboard[y][x].setPosition(x, y);
				lowestY--;
				y--;
			}

			// Add the word played by the player itself:
			words.add(new Word(highestX, lowestX, highestY, lowestY));
		} else if (highestY == lowestY && highestX != lowestX) {
			// the word is horizontal

			// Start by finding all words that "crosses" these player-owned
			// letters:
			for (GameLetter gl : enteredLetters) {
				int x = gl.getXIndex();
				int y = gl.getYIndex();
				int tempHY = y;
				int tempLY = y;

				if (y < 16 && !gameboard[y + 1][x].isDummy()) {
					tempHY = y + countDown(gl);
				}
				if (y > 0 && !gameboard[y - 1][x].isDummy()) {
					tempLY = y - countUp(gl);
				}

				if (tempHY != tempLY) {
					words.add(new Word(x, x, tempHY, tempLY));
				}

			}

			// Special case: there are letters after the word:
			int x = highestX + 1;
			int y = highestY;
			while (x <= 16 && !gameboard[y][x].isDummy()) {
				enteredLetters.add(gameboard[y][x]);
				gameboard[y][x].setPosition(x, y);
				highestX++;
				x++;
			}

			// Special case: there are letters in front of the word:
			x = lowestX - 1;
			while (x >= 0 && !gameboard[y][x].isDummy()) {
				enteredLetters.add(gameboard[y][x]);
				gameboard[y][x].setPosition(x, y);
				lowestX--;
				x--;
			}

			// Add the word played by the player itself:
			words.add(new Word(highestX, lowestX, highestY, lowestY));

		} else if (highestY == lowestY && highestX == lowestX) {
			// Only one letter was placed by the player
			if (isBoardEmpty()) {
				// Add the word played by the player itself:
				words.add(new Word(highestX, lowestX, highestY, lowestY));
			} else {
				// check for a vertical word:
				GameLetter gl = enteredLetters.get(0);
				int x = gl.getXIndex();
				int y = gl.getYIndex();
				int tempHY = y;
				int tempLY = y;

				if (y < 16 && !gameboard[y + 1][x].isDummy()) {
					tempHY = y + countDown(gl);
				}
				if (y > 0 && !gameboard[y - 1][x].isDummy()) {
					tempLY = y - countUp(gl);
				}

				if (tempHY != tempLY) {
					words.add(new Word(x, x, tempHY, tempLY));
				}

				// check for a horizontal word:
				x = gl.getXIndex();
				y = gl.getYIndex();
				int tempLX = x;
				int tempHX = x;
				if (x < 16 && !gameboard[y][x + 1].isDummy()) {
					tempHX = x + countRight(gl);
				}
				if (x > 0 && !gameboard[y][x - 1].isDummy()) {
					tempLX = x - countLeft(gl);
				}

				if (tempHX != tempLX) {
					words.add(new Word(tempHX, tempLX, y, y));
				}
			}
		}

		return words;
	}

	// Count the number of letters "above" the given letter on the game board:
	private int countUp(GameLetter gl) {
		int x = gl.getXIndex();
		int y = gl.getYIndex();

		for (int i = y - 1; i >= 0; i--) {
			if (gameboard[i][x].isDummy()) {
				return y - i;
			}
		}
		return y - 0;
	}

	// Count the number of letters "below" the given letter on the game board:
	private int countDown(GameLetter gl) {
		int x = gl.getXIndex();
		int y = gl.getYIndex();

		for (int i = y + 1; i < 17; i++) {
			if (gameboard[i][x].isDummy()) {
				return i - y;
			}
		}
		return 16 - y;
	}

	// Count the number of letters "to the left of" the given letter on the game
	// board:
	private int countLeft(GameLetter gl) {
		int x = gl.getXIndex();
		int y = gl.getYIndex();

		for (int i = x - 1; i >= 0; i--) {
			if (gameboard[y][i].isDummy()) {
				return x - i;
			}
		}
		return x - 0;
	}

	// Count the number of letters "to the right of" the given letter on the
	// game board:
	private int countRight(GameLetter gl) {
		int x = gl.getXIndex();
		int y = gl.getYIndex();

		for (int i = x + 1; i < 17; i++) {
			if (gameboard[y][i].isDummy()) {
				return i - x;
			}
		}
		return 16 - x;
	}

	private boolean isBoardEmpty() {
		for (GameLetter[] glRow : gameboard) {
			for (GameLetter gl : glRow) {
				if (!gl.isDummy()) {
					return false;
				}
			}
		}
		return true;
	}
}
